package com.ifacebox.software.updater;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Properties;
import java.util.concurrent.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;


/**
 * @author znn
 */
public class SoftwareUpdaterService {
    private static final Log log = LogFactory.getLog(SoftwareUpdaterService.class);
    public static final String SOFTWARE_UPDATER_ADDRESS = "http://software-updater.ifacebox.com";
    public static final String SOFTWARE_UPDATER_NAME = "facebox创视-%s.exe";
    private static final int CORE_POOL_SIZE = 1;
    private static final int MAX_NUM_POOL_SIZE = 1;
    private static final long KEEP_ALIVE_TIME = 0;
    private static final int ONE_SECOND_TIME = 1000;
    private static final int CONNECT_TIMEOUT = ONE_SECOND_TIME * 30;
    private static final int READ_TIMEOUT = ONE_SECOND_TIME * 30;
    private static final int BUFFER_SIZE = 1024 * 64;
    private static final String SOFTWARE_MD5 = "MD5";
    private static final String SOFTWARE_UPDATER_CONTENT_MD5 = "Content-MD5";
    private static final String SOFTWARE_UPDATER_PATH_END_WITH = ".jar";
    private final SoftwareUpdaterConfig softwareUpdaterConfig;
    private SoftwareUpdaterCallback softwareUpdaterCallback;
    private final ExecutorService executorService;
    private volatile long fileUpdaterSoftwareLastModified;

    public SoftwareUpdaterService(SoftwareUpdaterConfig softwareUpdaterConfig) {
        this.softwareUpdaterConfig = softwareUpdaterConfig;
        this.executorService = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_NUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS
                , new LinkedBlockingQueue<>(), r -> new Thread(r, "ShowSoftwareUpdater-Thread"));
        FileUtils.deleteQuietly(this.softwareUpdaterConfig.getFileUpdaterSoftware());
    }

    public SoftwareUpdaterService(String softwareUpdaterAddress, String softwareUpdaterName) {
        this(new SoftwareUpdaterConfig(softwareUpdaterAddress, softwareUpdaterName));
    }

    public synchronized boolean updater(SoftwareUpdaterCallback softwareUpdaterCallback) {
        if (this.executorService.isTerminated()) {
            return false;
        }
        this.softwareUpdaterCallback = softwareUpdaterCallback;
        this.executorService.execute(this::updater);
        return true;
    }

    public void updater() {
        HttpURLConnection connection = null;
        if (log.isInfoEnabled()) {
            log.info(String.format("访问服务器升级文件地址：【%s】", this.softwareUpdaterConfig.getSoftwareUpdaterAddress()));
        }
        long ms = System.currentTimeMillis();
        try {
            // 准备升级
            this.softwareUpdaterCallback.onSoftwareUpdaterPrepared(this.softwareUpdaterConfig);
            if (log.isInfoEnabled()) {
                log.info(String.format("准备下载升级文件：【%s】", this.softwareUpdaterConfig.toString()));
            }
            // 创建连接
            try {
                connection = this.createConnection();
            } catch (Exception e) {
                throw new SoftwareUpdaterException("连接升级服务器失败！", e);
            }
            // 获取HTTP相应头中的MD5
            if (log.isInfoEnabled()) {
                log.info(String.format("HTTP-HEADER-FIELDS：【%s】", connection.getHeaderFields().toString()));
            }
            // 第一次下载获取文件MD5、修改时间、文件大小
            if (this.softwareUpdaterConfig.getFileUpdaterSoftware().length() == 0) {
                this.softwareUpdaterConfig.setFileUpdaterSoftwareDigest(null);
                try {
                    this.softwareUpdaterConfig.setFileUpdaterSoftwareDigest(this.readSoftwareUpdaterDigest(connection));
                } catch (Exception e) {
                    throw new SoftwareUpdaterException("获取服务器升级文件MD5失败！", e);
                }
                if (log.isInfoEnabled()) {
                    log.info(String.format("获取服务器升级文件MD5：【%s】", this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest()));
                }
                if (this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest() == null || this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest().isEmpty()) {
                    throw new SoftwareUpdaterException("获取服务器升级文件MD5错误！");
                }
                this.fileUpdaterSoftwareLastModified = connection.getLastModified();
            }
            if (log.isInfoEnabled()) {
                log.info(String.format("获取服务器升级文件修改时间：【%s/%s】", this.fileUpdaterSoftwareLastModified, connection.getLastModified()));
            }
            // 断点续传，验证升级文件修改时间
            if (this.fileUpdaterSoftwareLastModified != connection.getLastModified()) {
                FileUtils.deleteQuietly(this.softwareUpdaterConfig.getFileUpdaterSoftware());
                throw new SoftwareUpdaterException("服务器升级文件与下载文件不一致！");
            }
            // 获取上次安装完后生成.ini文件中的MD5
            this.softwareUpdaterConfig.setFileSetupSoftwareDigest(null);
            if (this.softwareUpdaterConfig.getFileSetupSoftware().exists()) {
                try {
                    this.softwareUpdaterConfig.setFileSetupSoftwareDigest(this.readSetupSoftwareDigest(this.softwareUpdaterConfig.getFileSetupSoftware()));
                } catch (Exception e) {
                    throw new SoftwareUpdaterException("获取上次升级文件MD5失败！", e);
                }
            }
            if (log.isInfoEnabled()) {
                log.info(String.format("获取上次升级文件MD5：【%s】", this.softwareUpdaterConfig.getFileSetupSoftwareDigest()));
            }
            // 判断服务器升级文件MD5与上次安装升级文件MD5是否一致
            boolean isSoftwareUpdater = !this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest().equals(this.softwareUpdaterConfig.getFileSetupSoftwareDigest());
            if (!isSoftwareUpdater && log.isInfoEnabled()) {
                log.info(String.format("服务器升级文件MD5与上次升级文件MD5一致不需要升级：【%s/%s】", this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest(), this.softwareUpdaterConfig.getFileSetupSoftwareDigest()));
            }
            this.updater(connection, isSoftwareUpdater);
        } catch (SoftwareUpdaterException e) {
            if (log.isErrorEnabled()) {
                log.error(String.format("软件升级异常：【%s】", e.getErrorMsg()), e);
            }
            this.softwareUpdaterCallback.onSoftwareUpdaterFailed(e);
        } finally {
            IOUtils.close(connection);
            if (log.isInfoEnabled()) {
                log.info(String.format("下载升级文件完成耗时：【%s】", (System.currentTimeMillis() - ms)));
            }
        }
    }

    private void updater(HttpURLConnection connection, boolean isSoftwareUpdater) throws SoftwareUpdaterException {
        // 开始升级，返回false用户取消升级
        if (!this.softwareUpdaterCallback.onSoftwareUpdaterStart(isSoftwareUpdater, softwareUpdaterConfig)) {
            if (log.isInfoEnabled()) {
                log.info(String.format("取消下载升级文件：【%s】", isSoftwareUpdater));
            }
            this.softwareUpdaterCallback.onSoftwareUpdaterFinish(null);
        } else {
            // 上次下载的升级文件是否存在
            this.softwareUpdaterConfig.setFileSoftwareDigest(null);
            if (this.softwareUpdaterConfig.getFileSoftware().exists()) {
                try {
                    this.softwareUpdaterConfig.setFileSoftwareDigest(this.readSoftwareUpdaterDigest(this.softwareUpdaterConfig.getFileSoftware()));
                } catch (Exception e) {
                    throw new SoftwareUpdaterException("获取本地升级文件MD5失败！", e);
                }
            }
            if (log.isInfoEnabled()) {
                log.info(String.format("获取本地升级文件MD5：【%s】", this.softwareUpdaterConfig.getFileSoftwareDigest()));
            }
            // 上次下载的升级文件MD5与服务器升级文件MD5一致，就直接安装
            if (this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest().equals(this.softwareUpdaterConfig.getFileSoftwareDigest())) {
                if (log.isInfoEnabled()) {
                    log.info(String.format("服务器升级文件MD5与本地升级文件MD5一致直接安装：【%s/%s】", this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest(), this.softwareUpdaterConfig.getFileSoftwareDigest()));
                }
            } else {
                if (log.isInfoEnabled()) {
                    log.info(String.format("开始下载升级文件：【%s】", isSoftwareUpdater));
                }
                this.updater(connection);
            }
            this.softwareUpdaterCallback.onSoftwareUpdaterFinish(this.softwareUpdaterConfig);
        }
    }

    private void updater(HttpURLConnection connection) throws SoftwareUpdaterException {
        // 下载
        try {
            this.writeSoftwareUpdater(connection);
        } catch (Exception e) {
            throw new SoftwareUpdaterException("下载升级文件失败！", e);
        }
        this.softwareUpdaterConfig.setFileSoftwareDigest(null);
        try {
            this.softwareUpdaterConfig.setFileSoftwareDigest(this.readSoftwareUpdaterDigest(this.softwareUpdaterConfig.getFileUpdaterSoftware()));
        } catch (Exception e) {
            throw new SoftwareUpdaterException("获取下载升级文件MD5失败！", e);
        }
        if (log.isInfoEnabled()) {
            log.info(String.format("获取下载升级文件MD5：【%s】", this.softwareUpdaterConfig.getFileSoftwareDigest()));
        }
        if (!this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest().equals(this.softwareUpdaterConfig.getFileSoftwareDigest())) {
            throw new SoftwareUpdaterException("校验下载升级文件MD5错误！");
        }
        if (log.isInfoEnabled()) {
            log.info(String.format("校验下载升级文件MD5：【%s/%s】", this.softwareUpdaterConfig.getFileUpdaterSoftwareDigest(), this.softwareUpdaterConfig.getFileSoftwareDigest()));
        }
        if (this.softwareUpdaterConfig.getFileSoftware().exists()) {
            if (!this.softwareUpdaterConfig.getFileSoftware().delete()) {
                throw new SoftwareUpdaterException("删除本地升级文件错误！");
            }
            if (log.isInfoEnabled()) {
                log.info(String.format("删除本地升级文件：【%s】", this.softwareUpdaterConfig.getFileSoftware().getName()));
            }
        }
        if (!this.softwareUpdaterConfig.getFileUpdaterSoftware().renameTo(this.softwareUpdaterConfig.getFileSoftware())) {
            throw new SoftwareUpdaterException("重命名下载升级文件错误！");
        }
        if (log.isInfoEnabled()) {
            log.info(String.format("重命名下载升级文件：【%s/%s】", this.softwareUpdaterConfig.getFileUpdaterSoftware().getName(), this.softwareUpdaterConfig.getFileSoftware().getName()));
        }
    }

    public HttpURLConnection createConnection() throws Exception {
        HttpURLConnection connection = (HttpURLConnection) new URL(this.softwareUpdaterConfig.getSoftwareUpdaterAddress()).openConnection();
        connection.setConnectTimeout(CONNECT_TIMEOUT);
        connection.setReadTimeout(READ_TIMEOUT);
        connection.setRequestProperty("Range", String.format("bytes=%d-", this.softwareUpdaterConfig.getFileUpdaterSoftware().length()));
        if (HttpURLConnection.HTTP_PARTIAL != connection.getResponseCode()) {
            throw new Exception(String.valueOf(connection.getResponseCode()));
        }
        return connection;
    }

    public String readSoftwareUpdaterDigest(HttpURLConnection connection) {
        String field = connection.getHeaderField(SOFTWARE_UPDATER_CONTENT_MD5);
        return Hex.encodeHexString(Base64.decodeBase64(field)).toUpperCase();
    }

    public String readSoftwareUpdaterDigest(File file) throws Exception {
        try (FileInputStream fis = new FileInputStream(file)) {
            return DigestUtils.md5Hex(fis).toUpperCase();
        }
    }

    public String readSetupSoftwareDigest(File file) throws Exception {
        try (FileInputStream fis = new FileInputStream(file)) {
            Properties props = new Properties();
            props.load(fis);
            return props.getProperty(SOFTWARE_MD5, "").toUpperCase();
        }
    }

    public void writeSoftwareUpdater(HttpURLConnection connection) throws Exception {
        try (InputStream inputStream = connection.getInputStream();
             RandomAccessFile outputStream = new RandomAccessFile(this.softwareUpdaterConfig.getFileUpdaterSoftware(), "rw")) {
            long totalWrite = this.softwareUpdaterConfig.getFileUpdaterSoftware().length(), secondWrite = 0, secondWriteTemp = 0;
            long totalSize = totalWrite + connection.getContentLengthLong();
            if (log.isInfoEnabled()) {
                log.info(String.format("升级文件下载大小：【%s】", totalWrite));
                log.info(String.format("升级文件剩余大小：【%s】", connection.getContentLengthLong()));
                log.info(String.format("升级文件总大小：【%s】", totalSize));
            }
            outputStream.seek(totalWrite);
            long ms = System.currentTimeMillis();
            byte[] buffer = new byte[BUFFER_SIZE];
            for (int len; -1 != (len = inputStream.read(buffer)); ) {
                outputStream.write(buffer, 0, len);
                totalWrite += len;
                if (Math.abs(System.currentTimeMillis() - ms) < ONE_SECOND_TIME) {
                    secondWriteTemp += len;
                } else {
                    ms = System.currentTimeMillis();
                    secondWrite = secondWriteTemp;
                    secondWriteTemp = 0;
                }
                this.softwareUpdaterCallback.onSoftwareUpdaterChanged(totalSize, totalWrite, secondWrite);
            }
        }
    }

    public void install(File file) {
        this.install(file, true);
    }

    public void install(File file, boolean isSilent) {
        if (file.exists()) {
            try {
                String cmd = "\"" + file.getAbsolutePath() + "\"" + (isSilent ? " /silent" : "");
                if (log.isInfoEnabled()) {
                    log.info(String.format("运行安装升级文件：【%s】", cmd));
                }
                Runtime.getRuntime().exec(cmd);
            } catch (Exception e) {
                if (log.isErrorEnabled()) {
                    log.error(String.format("运行安装升级文件异常：【%s】", e.getMessage()), e);
                }
            }
        }
    }

    public static File getSoftwareUpdaterFile(String softwareUpdaterName) {
        String softwareUpdaterPath = SoftwareUpdaterService.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        try {
            softwareUpdaterPath = URLDecoder.decode(softwareUpdaterPath, "UTF-8");
        } catch (Exception e) {
            //
        }
        if (softwareUpdaterPath.endsWith(SOFTWARE_UPDATER_PATH_END_WITH)) {
            softwareUpdaterPath = softwareUpdaterPath.substring(0, softwareUpdaterPath.lastIndexOf("/") + 1);
        }
        return new File(softwareUpdaterPath, softwareUpdaterName);
    }

    public static String formatDuration(long millis) {
        long day = millis / 1000 / 60 / 60 / 24;
        long hour = millis / 1000 / 60 / 60 % 24;
        long minute = millis / 1000 / 60 % 60;
        long second = millis / 1000 % 60;
        return millis < 1000 ? "0秒"
                : ((day > 0 ? day + "天" : "")
                + (hour > 0 ? hour + "小时" : "")
                + (minute > 0 ? minute + "分钟" : "")
                + (second > 0 ? second + "秒" : ""));
    }

}
